/*
 The ASPNode class handles processing the ASP pages and executing them. '.js' files are executed directly without
 processing while any other file is process to separate the content from the server side JavaScript file to be executed.
 Source code within these '<?' '?>' tags is treated as source code, everything else is sent to the client as is.
 
 
 Date: 2014-08-03
 Author: Charbel Choueiri
 Email: charbel.choueiri@live.ca
*/
function ASPNode(config)
{
	var self = this;

	this.openTag = '<?';
	this.closeTag = '?>';

	/*
	 Initialize the class. Constructor. 
	*/
	this.init = function() { };

	/*
	  Process the given request. It expects the request.file = {root, path } to exist. This will create the context in which all scripts and inner
	  scripts are executes.
	*/
	this.process = function(request, response)
	{
		// The header is sent just before the first response.write. This boolean checks if the header has been sent yet, if not sends it and turns true.
		var headerSent = false;
		
		// Create the sandbox/context in which this file will be processed.
		var context = {				
			__content: [],
			
			// The scripts have direct access to the header that gets written to the client. The header is sent when the first 'write' call occurs.
			header: {'content-type': 'text/html' },
			
			// Also the request and response is sent to the client.
			request: request,
			response: response,

			// If the header has not yet been sent to the client, then sends it. Writes the given data directly to the response.
			// Writing directly to the output steam saves memory and speeds up the client loading the page.
			write: function(data)
			{
				if (!headerSent) { headerSent = true; response.writeHead(200, context.header); }
				response.write(data.toString());
			},

			// Calls write and adds CRLF to the end of the given data.
			writeLine: function(data) { context.write(data + "\n\r"); },

			// The 'require' method allows scripts to use the functionality and resolve local file paths. 
			// It is highly recommended to use include instead because the included files do not have access to the context.
			require: function(file)
			{
				// If the file path starts with './' or '../' then it is a local defined module. resolve the path and load it.
				if (file.indexOf('./') == 0 || file.indexOf('../') == 0) file = Path.resolve(request.file.root, file);

				// Load the module and return it.
				return module.require(file);
			},

			// Executes the given file within the current context. If it is a '.js' file, it is executed as is, otherwise it is assumed to be an ASP page. See 'this.execute'
			include: function(file)
			{
				// Resolve the file path relative to the root of the website.
				var path = Path.resolve(request.file.root, file);
				
				// Execute the file within the same context.
				self.execute(context, path);
			}
		};
		
		// Create a VM context so it can be used throughout any includes within the executed scripts.
		context = VM.createContext(context, context.request.file.path);
		
		// Execute the file in the created context.
		self.execute(context, context.request.file.path);
		
		// Finally close the connection to indicate to the browser that there is no more data to transmit.
		response.end();
	};

	/*
	  Executes the given file within the given context. If the file is '.js' file it executed directly, otherwise
	  it is treated as an ASP file. See this.getSections.
	*/
	this.execute = function(context, filePath)
	{
		// TODO: check for file reading errors and that file exists.
		// TODO: if cash is enabled then check the cash first. Also save the file in the cash if it does not exist

		// Get the file content.
		var content = FS.readFileSync(filePath);
		
		// If the file extension is '.js' then use the file as is, otherwise get the different sections.
		if (Path.extname(filePath) != '.js')
		{
			// Break up the requested file content into the source and content sections.
			var sections = self.getSections(context.__content.length, new String(content));
			
			// Set the content to the source to be executed.
			content = sections.source;
			
			// Place the content array within the contexts so it can be accessed when executing the file.
			context.__content.push(sections.content);
		}

		// Execute the script within the context.
		VM.runInContext(content, context, filePath);
	};


	/*
	 Splits the given file content into the content sections and the source code sections.

	 NOTE: This expects a 'write' method to exist within the context of the executed code
	 and it should have access to '__content[id][]' array. The caller must place the returned
	 content within the __content array under the given id. 

	 id: is used to identify specific content for a specific script. This provides the ability
	 to cash already processed scripts and reuse them without the need to recompile.

	 Content: is the string content of the file.

	 Returns: an object with the following format: { id: <INTEGER>, source: <STRING>, content: [<STRING>] } 
	 Where the id is the specified integer id.
	 Content is an ordered array of the different content sections of the file that is
	 referred to by the source code to be written to the output stream at specific points to
	 maintain the flow of the code.
	*/
	this.getSections = function(id, content)
	{
		var sections = { id: id, source: '', content: []};
		var endIndex = 0;
		var startIndex = 0;

		// Read through all the given content looking for for <? ... ?> or <?= ... ?> sections.
		while (endIndex < content.length)
		{
			// Find the next index of the start tag.
			var endIndex = content.indexOf(self.openTag, startIndex);

			// If found code section then find the end of it and append it to the blocks.
			if (endIndex >= 0)
			{
				// If there was content before the start tag then read them first.
				if (endIndex > startIndex)
				{
					// Append a read command to the source code referencing the current content array location.
					sections.source += 'write( __content[' + id + '][' + sections.content.length + '] );';
					
					// Next get the section of data from the section and add to the the content array in the expected location.
					sections.content.push( content.slice(startIndex, endIndex) );
				}

				// Skip the open tag.
				startIndex = endIndex + self.openTag.length;
				
				// If the next character is = then the source code is to be outputted to the stream.
				var writeSection = (content[startIndex] == '=' ? startIndex++ : -1);

				// Find the close tag.
				endIndex = content.indexOf(self.closeTag, startIndex);

				// If end tag exists then capture the block of code and append it to the source.
				if (endIndex >= 0)
				{
					// If the code block was preceded by '<?=' then encapsulate it with a 'write' call so the result can be written to the output stream.
					if (writeSection > 0) sections.source += 'write( ' + content.slice(startIndex, endIndex) + ' );';

					// Otherwise place the code as is. Ensure there is ';' at the end.
					else sections.source += content.slice(startIndex, endIndex) + ';'

					// Move the start index forward past the close tag.
					startIndex = endIndex + self.closeTag.length;
				}
				// If the close tag was not found then throw exception. TODO: get the line number of start tag for more detailed error reporting.
				else throw new Exception('Missing close tag ?>'); 				
			}			
			// If a start tag <? was not found then the rest fo the file is just text content.
			else
			{
				// Move the end tag to the end of the stream.
				endIndex = content.length;
				
				// Add a write call to the source code referencing the content array.
				sections.source += 'write( __content[' + id + '][' + sections.content.length + '] );';
				sections.content.push( content.slice(startIndex, endIndex) );
			}
		}
		
		// Finally return all the source and content sections.
		return sections;
	};


	// Call the constructor.
	this.init();
}


// Load required modules.
var VM = require('vm');
var FS = require('fs');
var Path = require('path');

// Make the entire class visible to the outside.
module.exports = { create: ASPNode };

